package com.harry.configuration.lifecycle;

import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.persistence.Column;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;

import org.apache.commons.collections4.SetUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.hibernate.annotations.Fetch;
import org.hibernate.annotations.FetchMode;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.InitializingBean;

import com.harry.basis.BasePoEntity;
import com.harry.utility.InnerUtils;
import com.harry.utility.PoEntityHelper;
import com.harry.utility.inflector.Inflector;

import io.bretty.console.table.Alignment;
import io.bretty.console.table.ColumnFormatter;
import io.bretty.console.table.Table;
import io.bretty.console.table.Table.Builder;

/**
 * 启动前
 */
public class BeforeAppLaunch implements InitializingBean {

	private static boolean runned = false;

	@Override
	public void afterPropertiesSet() throws Exception {
		if (!runned && (runned = true)) {
			before();
		}
	}

	private void before() throws Exception {
		initEntitySettings();
	}

	private void initEntitySettings() throws Exception {
		List<String[]> consoleList = new LinkedList<>();
		appendConsole(consoleList, "Declaring Class", "Field Name", "Mapping Type", "Local Key", "Target Key",
				"Join Table");
		Set<Class<?>> classes = InnerUtils.getAllEntities();
		// each class
		for (Class<?> clazz : classes) {
			// each super of class
			for (Class<?> entityClass = clazz; BasePoEntity.class.isAssignableFrom(entityClass)
					&& BasePoEntity.class != entityClass; entityClass = entityClass.getSuperclass()) {
				// each field of class and its super
				Field[] fields = entityClass.getDeclaredFields();
				// OneToOne parent-child
				if (Stream.of(fields)
						.filter(f -> StringUtils.equals("child", f.getName()) || StringUtils.equals("parent", f.getName()))
						.count() == 2l) {
					linkedListStructure(entityClass, fields, consoleList);
					fields = Stream.of(fields).filter(f -> !f.getName().equals("parent") && !f.getName().equals("child"))
							.collect(Collectors.toList()).toArray(ArrayUtils.toArray());
				}
				// OneToMany parent-children
				else if (Stream.of(fields)
						.filter(f -> StringUtils.equals("children", f.getName()) || StringUtils.equals("parent", f.getName()))
						.count() == 2l) {
					multiTreeStructure(entityClass, fields, consoleList);
					fields = Stream.of(fields).filter(f -> !f.getName().equals("parent") && !f.getName().equals("children"))
							.collect(Collectors.toList()).toArray(ArrayUtils.toArray());
				}
				for (Field field : fields) {
					// each annotation of field
					OneToOne o2o = field.getAnnotation(OneToOne.class);
					OneToMany o2m = field.getAnnotation(OneToMany.class);
					ManyToOne m2o = field.getAnnotation(ManyToOne.class);
					ManyToMany m2m = field.getAnnotation(ManyToMany.class);
					Column clm = field.getAnnotation(Column.class);
					JoinTable jtb = field.getAnnotation(JoinTable.class);
					JoinColumn jcl = field.getAnnotation(JoinColumn.class);
					Fetch fch = field.getAnnotation(Fetch.class);
					if (fch != null) {
						if (o2o != null || m2o != null) {
							setAnnotationValueByKeyIfAbsent(fch, "value", FetchMode.SELECT);
						} else if (o2m != null || m2m != null) {
							setAnnotationValueByKeyIfAbsent(fch, "value", FetchMode.SUBSELECT);
						} else {
							throw new Exception(
									"Entity settings error [" + entityClass.getName() + "." + field.getName() + "] at Fetch.");
						}
					}
					// ManyToMany
					if (m2m != null && jtb != null) {
						setAnnotationValueByKeyIfAbsent(jtb, "name", m2mTableName(clazz, field.getName()));
						setAnnotationValueByKeyIfAbsent(jtb.joinColumns()[0], "name", m2mSourceId(clazz, field.getName()));
						setAnnotationValueByKeyIfAbsent(jtb.inverseJoinColumns()[0], "name", m2mTargetId(clazz, field.getName()));
						appendConsole(consoleList, clazz.getSimpleName(), field.getName(), "ManyToMany",
								jtb.joinColumns()[0].name(), jtb.inverseJoinColumns()[0].name(), jtb.name());
					}
					// OneToMany
					else if (o2m != null && jcl != null) {
						Class<?> fieldComponentClass = PoEntityHelper
								.findComponentType(BeanUtils.getPropertyDescriptor(field.getDeclaringClass(), field.getName()));
						String fieldClassName = fieldComponentClass.getSimpleName();
						String fieldClassUnderScoreName = InnerUtils.toUnderScoreCase(fieldClassName);
						fieldClassUnderScoreName = fieldClassUnderScoreName.substring(fieldClassUnderScoreName.indexOf("_") + 1);
						fieldClassUnderScoreName = fieldClassUnderScoreName.toLowerCase();
						String fieldUnderScoreName = InnerUtils
								.toUnderScoreCase(Inflector.getInstance().singularize(field.getName()));
						String foreignKeyInTarget = InnerUtils.toUnderScoreCase(clazz.getSimpleName() + "Id");
						foreignKeyInTarget = foreignKeyInTarget.substring(foreignKeyInTarget.indexOf("_") + 1);
						int lastIndex = fieldUnderScoreName.lastIndexOf(fieldClassUnderScoreName);
						if (lastIndex == 0) {
							// skip;
						} else if (lastIndex > 0) {
							foreignKeyInTarget = fieldUnderScoreName.substring(0, lastIndex - 1) + "_" + foreignKeyInTarget;
						} else {
							foreignKeyInTarget = fieldUnderScoreName + "_id";
						}
						foreignKeyInTarget = foreignKeyInTarget.toUpperCase();
						setAnnotationValueByKeyIfAbsent(jcl, "name", foreignKeyInTarget);
						appendConsole(consoleList, clazz.getSimpleName(), field.getName(), "OneToMany", "ID", foreignKeyInTarget,
								"");
					}
					// AnyToOne
					else if ((o2o != null || m2o != null) && jcl != null) {
						setAnnotationValueByKeyIfAbsent(jcl, "name",
								InnerUtils.toUnderScoreCase(field.getName() + "Id").toUpperCase());
						appendConsole(consoleList, clazz.getSimpleName(), field.getName(), o2o != null ? "OneToOne" : "ManyToOne",
								jcl.name(), "ID", "");
					}
					// all null
					else if (o2o == null && o2m == null && m2o == null && m2m == null && clm == null && jtb == null
							&& jcl == null) {
						// skip
					}
					// basic field
					else if (clm != null) {
						// skip
					}
					//
					else {
						throw new Exception("Entity settings error [" + entityClass.getName() + "." + field.getName() + "]");
					}
				}
			}
		}
		Builder builder = null;
		for (int i = 0; i < consoleList.get(0).length; i++) {
			int ii = i;
			int width = consoleList.stream().map(ss -> ss[ii].length()).max((a, b) -> a - b).get() + 2;
			String[] datas = consoleList.stream().map(ss -> " " + ss[ii] + " ").collect(Collectors.toList())
					.toArray(ArrayUtils.toArray());
			if (builder == null) {
				builder = new Table.Builder("", datas, ColumnFormatter.text(Alignment.LEFT, width));
			} else {
				builder.addColumn("", datas, ColumnFormatter.text(Alignment.LEFT, width));
			}
		}
		String consoleStr = builder.build().toString();
		System.out.println(consoleStr.substring(consoleStr.indexOf("\n") + 1));
	}

	private void multiTreeStructure(Class<?> entityClass, Field[] fields, List<String[]> consoleList) throws Exception {
		Field childrenField = entityClass.getDeclaredField("children");
		Field parentField = entityClass.getDeclaredField("parent");
		JoinColumn childrenJcl = childrenField.getAnnotation(JoinColumn.class);
		JoinColumn parentJcl = parentField.getAnnotation(JoinColumn.class);
		setAnnotationValueByKeyIfAbsent(parentJcl, "name", "PARENT_ID");
		setAnnotationValueByKeyIfAbsent(childrenJcl, "name", "PARENT_ID");
		appendConsole(consoleList, entityClass.getSimpleName(), parentField.getName() + "<=>" + childrenField.getName(),
				"Multi-Tree", "ID", "PARENT_ID", "");
	}

	private void linkedListStructure(Class<?> entityClass, Field[] fields, List<String[]> consoleList) throws Exception {
		Field childField = entityClass.getDeclaredField("child");
		Field parentField = entityClass.getDeclaredField("parent");
		JoinColumn childJcl = childField.getAnnotation(JoinColumn.class);
		JoinColumn parentJcl = parentField.getAnnotation(JoinColumn.class);
		setAnnotationValueByKeyIfAbsent(parentJcl, "name", "PARENT_ID");
		setAnnotationValueByKeyIfAbsent(childJcl, "name", "ID");
		setAnnotationValueByKeyIfAbsent(childJcl, "referencedColumnName", "PARENT_ID");
		setAnnotationValueByKeyIfAbsent(childJcl, "insertable", false);
		setAnnotationValueByKeyIfAbsent(childJcl, "updatable", false);
		appendConsole(consoleList, entityClass.getSimpleName(), parentField.getName() + "<=>" + childField.getName(),
				"LinkedList", "ID", "PARENT_ID", "");
	}

	private String m2mSourceId(Class<?> clazz, String fieldName) {
		String underScoreSourceClassName = InnerUtils.toUnderScoreCase(clazz.getSimpleName());
		return underScoreSourceClassName.substring(underScoreSourceClassName.indexOf("_") + 1).toUpperCase() + "_ID";
	}

	private String m2mTargetId(Class<?> clazz, String fieldName) {
		return Inflector.getInstance().singularize(InnerUtils.toUnderScoreCase(fieldName)).toUpperCase() + "_ID";
	}

	private String m2mTableName(Class<?> clazz, String fieldName) {
		String underScoreSourceClassName = InnerUtils.toUnderScoreCase(clazz.getSimpleName());
		String res;
		if (isFieldNameMappingTargetClass(clazz, fieldName)) {
			PropertyDescriptor pd = BeanUtils.getPropertyDescriptor(clazz, fieldName);
			String underScoreTargetClassName = InnerUtils
					.toUnderScoreCase(PoEntityHelper.findComponentType(pd).getSimpleName());
			res = StringUtils
					.join(new TreeSet<String>(SetUtils.unmodifiableSet(underScoreSourceClassName, underScoreTargetClassName))
							.toArray(new String[0]), "_to_");
		} else {
			res = underScoreSourceClassName + "_to_"
					+ Inflector.getInstance().singularize(InnerUtils.toUnderScoreCase(fieldName));
		}
		return res.toUpperCase();
	}

	private boolean isFieldNameMappingTargetClass(Class<?> clazz, String fieldName) {
		PropertyDescriptor pd = BeanUtils.getPropertyDescriptor(clazz, fieldName);
		String singleFieldName = Inflector.getInstance().singularize(fieldName);
		Class<?> targetClass = PoEntityHelper.findComponentType(pd);
		String excludingPrefixSmallCamelCase = InnerUtils.toFirstLowerCase(
				targetClass.getSimpleName().substring(InnerUtils.toUnderScoreCase(targetClass.getSimpleName()).indexOf("_")));
		return StringUtils.equals(singleFieldName, excludingPrefixSmallCamelCase);
	}

	private void setAnnotationValueByKeyIfAbsent(Annotation anno, String key, Object value) throws Exception {
		InvocationHandler ih = Proxy.getInvocationHandler(anno);
		Field memberValuesField = ih.getClass().getDeclaredField("memberValues");
		memberValuesField.setAccessible(true);
		Object map = memberValuesField.get(ih);
		Object ovalue = PoEntityHelper.readValueByProperty(map, key);
		if (ovalue == FetchMode.JOIN || ovalue == null || StringUtils.equals("", ovalue.toString())) {
			PoEntityHelper.writeValueByProperty(map, key, value);
		}
	}

	private void appendConsole(List<String[]> consoleList, String simpleName, String name, String type, String local,
			String foreign, String table) {
		consoleList.add(ArrayUtils.toArray(simpleName, name, type, local, foreign, table));
	}
}
